package io.github.uicdb.badskill;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import io.github.uicdb.badskill.api.CostumerConsumer;
import io.github.uicdb.badskill.api.CostumerConsumerManager;
import io.github.uicdb.badskill.codec.JsonFactory;
import lombok.*;
import net.minecraft.ChatFormatting;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.ai.attributes.Attribute;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraftforge.registries.ForgeRegistries;
import org.jetbrains.annotations.Nullable;

import java.util.*;

import static net.minecraft.world.item.ItemStack.ATTRIBUTE_MODIFIER_FORMAT;

@EqualsAndHashCode
@ToString
@Builder
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class SkillOption {
    @Getter
    public ResourceLocation id;

    public long exp;
    @Nullable
    public List<ItemStack> itemStacks;
    @Nullable
    public List<ResConsumerData> consumerData;
    @Nullable
    public Multimap<Attribute, AttributeModifier> modifiers;

    private SkillOption(ResourceLocation id) {
        this.id = id;
    }

    public static SkillOption of(ResourceLocation id) {
        return new SkillOption(id);
    }

    public boolean learn(Player player, boolean sim) {
        if (player.totalExperience >= exp & checkInv(player, true) & checkConsumer(player, true)) {
            if (!sim) {
                player.giveExperiencePoints(Math.toIntExact(-exp));
                checkInv(player, false);
                checkConsumer(player, false);
                var capability = player.getCapability(PlayerSkillCapabilityProvider.CAPABILITY);
                if (modifiers != null && capability.isPresent()) {
                    modifiers.forEach(capability.resolve().orElseThrow()::addAttribute);
                }
            }
            return true;
        }
        return false;
    }

    public List<Component> getTooltips(Player player) {
        List<Component> list = new ArrayList<>();
        list.add(Component.translatable("tips.option.requirements").withStyle(ChatFormatting.ITALIC));
        if (exp != 0) {
            if (player.totalExperience >= exp) {
                list.add(Component.empty().append(Component.translatable("tips.option.require.exp", exp).withStyle(ChatFormatting.STRIKETHROUGH)));
            } else {
                list.add(Component.empty().append(Component.translatable("tips.option.require.exp", exp)));
            }
        }
        if (itemStacks != null) {
            itemStacks.forEach(stack -> {
                if (!isPlayerItemLessThan(stack, player))
                    list.add(Component.empty().append(Component.translatable("tips.option.require.item", stack.getDisplayName(), stack.getCount()).withStyle(ChatFormatting.STRIKETHROUGH)));
                else
                    list.add(Component.empty().append(Component.translatable("tips.option.require.item", stack.getDisplayName(), stack.getCount())));
            });
        }
        List<Component> rewards = new ArrayList<>();
        if (consumerData != null) {
            consumerData.forEach(data -> {
                CostumerConsumer consumer = CostumerConsumerManager.get(data.name, player);
                list.addAll(consumer.getTooltips(player, data.key, data.data, SkillOption.this, true));
                rewards.addAll(consumer.getTooltips(player, data.key, data.data, SkillOption.this, false));
            });
        }

        list.add(Component.translatable("tips.option.rewards").withStyle(ChatFormatting.ITALIC));
        if (modifiers != null) {
            addModifiersTips(list);
        }
        list.addAll(rewards);
        return list;
    }

    private void addModifiersTips(List<Component> list) {
        Multimap<Attribute, AttributeModifier> multimap = modifiers;
        assert multimap != null;
        if (!multimap.isEmpty()) {
            for (Map.Entry<Attribute, AttributeModifier> entry : multimap.entries()) {
                AttributeModifier attributemodifier = entry.getValue();
                double d0 = attributemodifier.getAmount();
                double d1;
                if (attributemodifier.getOperation() != AttributeModifier.Operation.MULTIPLY_BASE && attributemodifier.getOperation() != AttributeModifier.Operation.MULTIPLY_TOTAL) {
                    if (entry.getKey().equals(Attributes.KNOCKBACK_RESISTANCE)) {
                        d1 = d0 * 10.0D;
                    } else {
                        d1 = d0;
                    }
                } else {
                    d1 = d0 * 100.0D;
                }
                if (d0 > 0.0D) {
                    list.add(Component.translatable("attribute.modifier.plus." + attributemodifier.getOperation().toValue(), ATTRIBUTE_MODIFIER_FORMAT.format(d1), Component.translatable(entry.getKey().getDescriptionId())).withStyle(ChatFormatting.BLUE));
                } else if (d0 < 0.0D) {
                    d1 *= -1.0D;
                    list.add(Component.translatable("attribute.modifier.take." + attributemodifier.getOperation().toValue(), ATTRIBUTE_MODIFIER_FORMAT.format(d1), Component.translatable(entry.getKey().getDescriptionId())).withStyle(ChatFormatting.RED));
                }
            }
        }
    }

    public void retreated(Player player, boolean backResources) {
        if (backResources) {
            if (itemStacks != null)
                itemStacks.forEach(player.getInventory()::add);
        }
        if (consumerData != null) {
            consumerData.forEach(data -> {
                CostumerConsumer consumer = CostumerConsumerManager.get(data.name, player);
                consumer.retreated(player, data.key, data.data, SkillOption.this, backResources);
            });
        }
        var capability = player.getCapability(PlayerSkillCapabilityProvider.CAPABILITY);
        if (modifiers != null && capability.isPresent()) {
            modifiers.forEach(capability.resolve().orElseThrow()::removeAttribute);
        }
    }

    public void enable(Player player) {
        if (consumerData != null) {
            consumerData.forEach(data -> {
                CostumerConsumer consumer = CostumerConsumerManager.get(data.name, player);
                consumer.enable(player, data.key, data.data, SkillOption.this);
            });
        }
        var capability = player.getCapability(PlayerSkillCapabilityProvider.CAPABILITY);
        if (modifiers != null && capability.isPresent()) {
            modifiers.forEach(capability.resolve().orElseThrow()::addAttribute);
        }
    }

    public void tick(Player player) {
        if (consumerData != null) {
            consumerData.forEach(data -> {
                CostumerConsumer consumer = CostumerConsumerManager.get(data.name, player);
                consumer.tick(player, data.key, data.data, SkillOption.this);
            });
        }
    }

    public boolean checkConsumer(Player player, boolean sim) {
        if (consumerData == null) return true;
        for (ResConsumerData data : consumerData) {
            CostumerConsumer consumer = CostumerConsumerManager.get(data.name, player);
            if (!consumer.consume(player, data.key, data.data, this, sim)) return false;
        }
        if (!sim) {
            for (ResConsumerData data : consumerData) {
                CostumerConsumer consumer = CostumerConsumerManager.get(data.name, player);
                consumer.consume(player, data.key, data.data, this, false);
            }
        }
        return true;
    }

    public void disable(Player player) {
        if (consumerData != null) {
            consumerData.forEach(data -> {
                CostumerConsumer consumer = CostumerConsumerManager.get(data.name, player);
                consumer.disable(player, data.key, data.data, SkillOption.this);
            });
        }
        var capability = player.getCapability(PlayerSkillCapabilityProvider.CAPABILITY);
        if (modifiers != null && capability.isPresent()) {
            modifiers.forEach(capability.resolve().orElseThrow()::removeAttribute);
        }
    }

    private boolean matchItemStack(ItemStack matcher, ItemStack container) {
        return ItemStack.isSameItemSameTags(matcher, container);
    }

    private boolean isPlayerItemLessThan(ItemStack itemStack, Player player) {
        var inv = player.getInventory();
        int matchCount = 0;
        for (ItemStack item : inv.items) {
            if (matchItemStack(itemStack, item)) {
                matchCount += item.getCount();
            }
            if (matchCount >= itemStack.getCount())
                break;
        }
        return matchCount < itemStack.getCount();
    }

    public boolean checkInv(Player player, boolean sim) {
        if (itemStacks == null) return true;
        var inv = player.getInventory();
        if (sim) {
            for (ItemStack itemStack : itemStacks) {
                return !isPlayerItemLessThan(itemStack, player);
            }
        } else {
            for (ItemStack itemStack : itemStacks) {
                int matchCount = itemStack.getCount();
                for (ItemStack item : inv.items) {
                    if (matchItemStack(itemStack, item)) {
                        int need = Math.min(item.getCount(), matchCount);
                        matchCount -= need;
                        item.shrink(need);
                    }
                    if (matchCount <= 0)
                        break;
                }
            }
        }
        return true;
    }

    public SkillOption addItems(ItemStack... stacks) {
        if (itemStacks == null) itemStacks = new ArrayList<>();
        itemStacks.addAll(Arrays.asList(stacks));
        return SkillOption.this;
    }

    public SkillOption addConsumerData(ResConsumerData... data) {
        if (consumerData == null) consumerData = new ArrayList<>();
        consumerData.addAll(Arrays.asList(data));
        return SkillOption.this;
    }

    public SkillOption addAttributeModifiers(Attribute attribute, AttributeModifier... attributeModifiers) {
        if (modifiers == null) modifiers = HashMultimap.create();
        for (AttributeModifier attributeModifier : attributeModifiers) {
            modifiers.put(attribute, attributeModifier);
        }
        return SkillOption.this;
    }

    public SkillOption setExp(int exp) {
        this.exp = exp;
        return SkillOption.this;
    }

    public JsonElement toJson() {
        Gson gson = JsonFactory.create();
        return gson.toJsonTree(this);
    }

    public static SkillOption fromJson(JsonElement element) {
        Gson gson = JsonFactory.create();
        return gson.fromJson(element, SkillOption.class);
    }

    public static SkillOption fromNetwork(FriendlyByteBuf buf) {
        SkillOption option = SkillOption.of(buf.readResourceLocation());
        option.exp = buf.readLong();
        if (buf.readBoolean()) {
            option.itemStacks = buf.readList(FriendlyByteBuf::readItem);
        }
        if (buf.readBoolean()) {
            option.consumerData = buf.readList(ResConsumerData::fromNetwork);
        }
        if (buf.readBoolean()) {
            Multimap<Attribute, AttributeModifier> multimap = HashMultimap.create();
            var r = buf.readMap(buf12 ->
                            buf12.readRegistryIdUnsafe(ForgeRegistries.ATTRIBUTES),
                    buf1 -> buf1.readCollection(Lists::newArrayListWithCapacity,
                            buf11 -> {
                                UUID id = buf11.readUUID();
                                String name = buf11.readUtf();
                                double amount = buf11.readDouble();
                                AttributeModifier.Operation operation = buf11.readEnum(AttributeModifier.Operation.class);
                                return new AttributeModifier(id, name, amount, operation);
                            }));
            r.keySet().forEach(attribute -> multimap.putAll(attribute, r.get(attribute)));
            option.modifiers = multimap;
        }
        return option;
    }

    public void toNetwork(FriendlyByteBuf buf) {
        buf.writeResourceLocation(id);
        buf.writeLong(exp);
        buf.writeBoolean(itemStacks != null);
        if (itemStacks != null) {
            buf.writeCollection(itemStacks, FriendlyByteBuf::writeItem);
        }
        buf.writeBoolean(consumerData != null);
        if (consumerData != null) {
            buf.writeCollection(consumerData, (buf1, data) -> data.toNetwork(buf1));
        }
        buf.writeBoolean(modifiers != null);
        if (modifiers != null) {
            buf.writeMap(modifiers.asMap(),
                    (buf14, attribute) ->
                            buf14.writeRegistryIdUnsafe(ForgeRegistries.ATTRIBUTES, attribute),
                    (buf13, attributeModifiers) ->
                            buf13.writeCollection(attributeModifiers,
                                    (buf12, modifier) -> {
                                        buf12.writeUUID(modifier.getId());
                                        buf12.writeUtf(modifier.getName());
                                        buf12.writeDouble(modifier.getAmount());
                                        buf12.writeEnum(modifier.getOperation());
                                    }));

        }
    }

    @EqualsAndHashCode
    @ToString
    public static final class ResConsumerData {
        public ResourceLocation name;
        public String key;
        public double data;

        public ResConsumerData(ResourceLocation name, String key, double data) {
            this.name = name;
            this.key = key;
            this.data = data;
        }

        public static ResConsumerData fromNetwork(FriendlyByteBuf buf) {
            return new ResConsumerData(buf.readResourceLocation(), buf.readUtf(), buf.readDouble());
        }

        public static ResConsumerData of(String name, String key, double data) {
            return new ResConsumerData(new ResourceLocation(name), key, data);
        }

        public void toNetwork(FriendlyByteBuf buf) {
            buf.writeResourceLocation(name);
            buf.writeUtf(key);
            buf.writeDouble(data);
        }

        public ResourceLocation name() {
            return name;
        }

        public String key() {
            return key;
        }

        public double data() {
            return data;
        }

    }
}
